オプション解析モジュール click を使いこなそう
click について
ClickはFlaskマイクロフレームワークの基礎として使用されたPython Web開発ライブラリのコレクションのひとつで、オプション解析処理をしてくれる拡張モジュールです。
Command-Line Interface Creation Kit から命名されています。
click は拡張モジュールなのでインストールする必要があります。
conda コマンドは 実行環境によっては pip コマンドを使う場合もあります。
code: conda
$ conda install click
code: pip
$ pip install click
click.echo()
click.echo() はpythonの print() と同じように与えた文字列を表示します。
print() との大きな違いは、err=True を与えると標準エラー出力へ出力されることです。
code: click_echo.py
@click.command()
def cmd():
typer.echo("This message oputput to stdout.")
typer.echo("This message oputput to stderr.", err=True)
if __name__ == '__main__':
cmd()
code: bash
% python typer_echo.py
This message oputput to stdout.
This message oputput to stderr.
% python typer_echo.py 2>/dev/null
This message oputput to stdout.
click.style()
click.echo() はclick.style() と共に使うことで、カラーなど文字の装飾が簡単になります。
click.sytle(text, fg, bg, bold, dim, underline, blink, reverse, reset)
text: 任意の文字列
fg: 前景色/文字の色 ('red', 'green', 'yellow'など)
bg: 背景色
bold: ボールド表示
dim: 薄暗く表示する
underline: アンダーライン表示
blink: 点滅表示
reverse: 前景色、背景色の反転
reset: 設定のリセット、False にすると設定を引き継ぐ
code: click_style.py
import click
@click.command()
def cmd():
click.echo(click.style('Hello World.',
fg='green', bg='red', reset=False))
click.echo(click.style('Hello Again.'))
if __name__ == '__main__':
cmd()
click.launch()
click.launch() は引数に与えたURLをブラウザでオープンします。
code: click_launch.py
import click
def open_google():
click.echo("Opening Google...")
if __name__ == "__main__":
open_google)
オプション解析
@click.option() は、オプション解析を行った結果を変数にして、デコレートした関数に渡してくれます。
次の例は、ユーザからの文字列入力を受け付けて、--count オプションで与えた数値だけ繰り返すものです。
code: click_hello.py
import click
@click.command()
@click.option('-C', '--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name', help='The person to greet.')
def hello(count, name):
"""COUNTで与えた回数だけHelloする"""
for x in range(count):
click.echo(f'Hello {name}')
if __name__ == '__main__':
hello()
この例では、--count オプションを-C としても受け付けることができ、defaultでオプション引数のデフォルト値を設定しています。defaultで指定した型としてセットされますが、明示的にtype を指定することができます。
ユーザがコマンドラインで--name オプションを与えない場合は、promptで指示した文字列を表示して入力待ちとなります。
code: bash
$ python click_hello.py --help
COUNTで与えた回数だけHelloする
Options:
-C, --count INTEGER Number of greetings.
--name TEXT The person to greet.
--help Show this message and exit.
$ python click_hello.py
Your name: Jack
Hello Jack!
$ python click_hello.py --name='David'
Hello David!
$ python click_hello.py --count=2
Your name: Freddie
Hello Freddie!
Hello Freddie!
関数に記述した docstrings がヘルプ表示のときに使用されます。
デコレートされる関数を次のようにすると辞書として受け取ることができます。
code: click_hello2.py
import click
@click.command()
@click.option('-C', '--count', default=1, type=int, help='Number of greetings.')
@click.option('--name', prompt='Your name', type=str, help='The person to greet.')
def hello(**kwargs):
print(kwargs)
hello()
code: bash
$ python click_hello2.py
Your name: Feddie
{'count': 1, 'name': 'Feddie'}
オプションをフラグとして処理したい
click.option() で is_flag=True としておくと、そのオプションはフラグとして解析されます。
code: click_optflag.py
import click
@click.command()
@click.option('--debug', is_flag=True, help='DEBUG mode')
def cmd(debug):
click.echo( f'DEBUG mode {debug}')
if __name__ == '__main__':
cmd()
code: bash
$ python click_optflag.py --help
Options:
--debug DEBUG mode
--help Show this message and exit.
$ python click_optflag.py
DEBUG mode False
$ python click_optflag.py --debug
DEBUG mode True
@click.option() に hidden=True を与えると、そのオプションはヘルプメッセージに表示されなくなります。
オプション引数を可変長にする
@click.option() に nargs で指定した数値だけ値を取ることができます。
code: click_optmultiargs.py
import click
@click.command()
@click.option('-P', '--position', nargs=2, help='Geometory: X y')
def cmd(position):
click.echo( position )
if __name__ == '__main__':
cmd()
code: bash
$ python click_optmultivalue.py -P 1
Error: -P option requires 2 arguments
$ python click_optmultivalue.py -P 1 2
('1', '2')
$ python click_optmultivalue.py -P 1 2 3
Usage: click_optmultivalue.py OPTIONS Try "click_optmultivalue.py --help" for help.
Error: Got unexpected extra argument (3)
同じオプションを複数回指定することを許す
click.option() では、 multiple=True を与えておくと、そのオプションは複数回指定することができるようになります。デフォルトでは、同じオプションが複数回与えられた場合は、最後に与えられたオプションが有効になります。
code: click_multiopt.py
import click
@click.command()
@click.option('-m', '--message', multiple=True)
# @click.option('-m', '--message')
def cmd(message):
click.echo( message )
if __name__ == '__main__':
cmd()
code: bash
$ python click_multiopts.py -m Beer -m Wine
('Beer', 'Wine')
オプションが指定された回数を知りたい
click.option() で count=True を設定すると、そのオプションが指定された回数がセットされます。
code: click_optcount.py
import click
@click.command()
@click.option('-v', '--verbose', count=True, help='Verbosly Mode')
def cmd(verbose):
click.echo(f'verbose level: {verbose}')
if __name__ == '__main__':
cmd()
code: bash
$ python click_optcount.py
verbose level: 0
$ python click_optcount.py -v
verbose level: 1
$ python click_optcount.py -v -v
verbose level: 2
$ python click_optcount.py -vvvv
verbose level: 4
オプション引数を限定させたい
click.option() で choice を設定しておくと、指定されたオプション引数をチェックするようになります。
code: click_optchoice.py
import click
@click.command()
@click.option('-H', '--hash-type', type=click.Choice('md5', 'sha1')) def cmd(hash_type):
click.echo( hash_type )
if __name__ == '__main__':
cmd()
code: bash
$ python click_optchoice.py --help
Options:
--help Show this message and exit.
$ python click_optchoice.py -H md5
md5
$ python click_optchoice.py -H sha1
sha1
$ python click_optchoice.py -H sha256
Try "click_optchoice.py --help" for help.
Error: Invalid value for "-H" / "--hash-type": invalid choice: sha256. (choose from md5, sha1)
オプション引数の数値範囲を指定したい
click.option の type に click.IntRange() を与えると指定したオプション引数が、期待している数値範囲にあるかチェックしてくれます。
code: click_optrange.py
import click
max_cpus=8
@click.command()
@click.option('-N', '--ncpus', type=click.IntRange(1,max_cpus,clamp=True),
help=f'Set cpu numbers. (1...{max_cpus})')
def cmd(ncpus):
click.echo( ncpus )
if __name__ == '__main__':
cmd()
この例では、clamp=True が与えられているので、click.lntRange() で指定した数値を超えた場合は補正されます。
code: bash
$ python click_optrange.py --help
Options:
-N, --ncpus INTEGER RANGE Set cpu numbers. (1...8)
--help Show this message and exit.
$ python click_optrange.py -N 8
8
$ python click_optrange.py -N 10
8
$ python click_optrange.py -N -2
1
オプションでUUIDを扱いたい
UUID(Universally Unique Identifier) は、重複することがない識別子で、多くの場合は e6501a90-2a30-45aa-9a6f-bb2013264341 のような16進表記の文字列として使われます。
これをオプション引数として使用したいときは次のようtype=click.UUID とします。
code: click_optuuid.py
import click
@click.command()
@click.option("--uuid", type=click.UUID, required=True)
def cmd(uuid):
click.echo(f'UUID={uuid}')
if __name__ == '__main__':
cmd()
click は与えられたオプション引数の文字列をUUIDとして妥当かチェックしてからセットしてくれます。
code: bash
$ python click_optuuid.py --help
Options:
--help Show this message and exit.
$ python -c "import uuid; print(uuid.uuid4())"
7c02d20d-2ef6-497f-a4d2-ea19a666ef6d
$ python click_optuuid.py --uuid=7c02d20d-2ef6-497f-a4d2-ea19a666ef6d
uuid=7c02d20d-2ef6-497f-a4d2-ea19a666ef6d
$ python click_optuuid.py --uuid=7c02d20d-2ef6-497f-a4d2-ea19a666
Try "click_optuuid.py --help" for help.
Error: Invalid value for "--uuid": 7c02d20d-2ef6-497f-a4d2-ea19a666 is not a valid UUID value
オプションのプレフィックスを変更したい
Linuxでは、通常コマンドラインのオプションはひとつ、もしくは2つのマイナス記号(-) で始まるものですが、次のようにスラッシュ記号(/)で区切ってオプションを記述することで別の文字をオプションとすることができるようになります。
code: click_optprefix.py
import click
@click.command()
@click.option('+w/-w', default=True)
def cmd(w):
click.echo( 'writable=%s' % w )
if __name__ == '__main__':
cmd()
code: bash
$ python click_optprefix.py --help
Options:
+w / -w
--help Show this message and exit.
$ python click_optprefix.py
writable=True
$ python click_optprefix.py -w
writable=False
$ python click_optprefix.py +w
writable=True
Windowsスタイルのオプシン
Windowsのコマンドの多くはスラッシュ記号(/) で始まる文字列をオプションとしていますが、このようなときはセミコロン(;) でオプションを区切って記述します。
code: click_optwinstyle.py
import click
@click.command()
@click.option('/debug;/no-debug')
def cmd(debug):
click.echo( 'debug=%s' % debug )
if __name__ == '__main__':
cmd()
オプション解析時にコールバック関数を与えたい
click.option() に callback=関数オブジェクトID を与えると、指定した関数を呼び出してくれます。
code: click_optcallback.py
import click
def print_version(ctx, param, value):
if not value or ctx.resilient_parsing:
return
click.echo('Version 1.0')
ctx.exit()
@click.command()
@click.option('--version', callback=print_version,
is_flag=True, expose_value=False, is_eager=True)
def cmd():
click.echo('command was done.')
if __name__ == '__main__':
cmd()
is_eager=True は他のオプションより優先度を高くします。
expose_value=False はデコレートした関数に値を渡しません。この値を True にすると関数cmd() はオプション解析処理の結果を受け取るための引数を指定している必要があります。
コールバック関数でオプションで指定された値のチェックを行うといった処理に使用します。
code: cick_optcallback_validate.py
import click
def validate_rolls(ctx, param, value):
try:
rolls, dice = map(int, value.split('d', 2))
return (dice, rolls)
except ValueError:
raise click.BadParameter('rolls need to be in format NdM')
@click.command()
@click.option('--rolls', callback=validate_rolls, default='1d6')
def roll(rolls):
click.echo(f'Rolling a {rolls0}-sided dice {rolls1} time(s)') if __name__ == '__main__':
roll()
確認を求めるようなオプション処理
コールバック関数を使うと確認を求めてようなオプションを作ることができます。
code: click_optcallback_yesno.py
import click
def abort_if_false(ctx, param, value):
if not value:
ctx.abort()
@click.command()
@click.option('--yes', is_flag=True, callback=abort_if_false,
expose_value=True,
prompt='Are you sure you want to drop the db?')
def cmd(yes):
click.echo('Dropped all tables!')
if __name__ == '__main__':
cmd()
これを少しスマートにしてくれるデコレータが @click.confirmation_option() です。
code: click_confirm.py
import click
@click.command()
@click.confirmation_option(prompt='Are you sure you want to drop the db?')
def cmd():
click.echo('Dropped all tables!')
if __name__ == '__main__':
cmd()
パスワード入力の処理をしたい
click.option() で hide_input=True とすると入力中の文字のエコー表示をしなくなります。また、confirmation_prompt=True にしておくと2度入力を求めて同じ場合にだけ、ユーザが入力した文字がセットされます。
code: click_optpassword.py
import click
@click.command()
@click.option('--password', prompt='Password',
hide_input=True, confirmation_prompt=True)
def cmd(password):
click.echo( password )
if __name__ == '__main__':
cmd()
これは、@clicj.password_option() デコレーターを使っても同じことができます。
code: click_passwordsmart.py
import click
@click.command()
@click.password_option()
def cmd(password):
click.echo( password )
if __name__ == '__main__':
cmd()
コマンド引数を処理したい
@click.argument() でデコレートするとコマンド引数を処理することができます。
code: click_argument.py
import click
@click.command()
@click.argument('src', nargs=-1, required=True)
@click.argument('dst', nargs=1)
def copy(src, dst):
"""Move file SRC to DST."""
for filename in src:
click.echo(f'move {filename} to folder {dst}')
if __name__ == '__main__':
copy()
@click.argument() に期待する引数の数をnargs で与えます。nargs=-1 とした引数は0もしくはそれ以上という意味になり上限がなくなります。最低1つは引数を期待する場合は、required=True と与えておきます。
コマンド引数をファイルとして扱いたい
click_argument.py の例は、引数を文字列として受けとるだけですが、このコードはファイル名を期待しているはずです。
このようなときは、次のようにtype にclick.File()を与えます。
code:click_argument_as_file.py
import click
@click.command()
@click.argument('srcfile', type=click.File('r'))
def cmd(srcfile):
lines = srcfile.readlines()
for line in lines:
if __name__ == '__main__':
cmd()
click.File() は引数で指定された文字列をファイル名として扱い、オープンをしたファイルオブジェクトをセットします。エラーチェックは自動的に行ってくれますが、ファイルの存在チェックまではしません。
code: bash
$ python click_argument_as_file.py /tmp/junk.txt
Usage: click_argument_as_file.py OPTIONS SRCFILE Try "click_argument_as_file.py --help" for help.
Error: Invalid value for "SRCFILE": Could not open file: /tmp/junk.txt: No such file or directory
$ echo 'Think Big.' > /tmp/junk.txt
$ python click_argument_as_file.py /tmp/junk.txt
Think Big.
type に click.File() ではなく、click.Path() を指定するすると、引数をファイルパスとして扱います。
code: click_argument_as_filepath.py
import click
@click.command()
@click.argument('srcfile', type=click.Path(exists=True))
def cmd(srcfile):
print(type(srcfile))
click.echo( click.format_filename(srcfile) )
if __name__ == '__main__':
cmd()
exists=True を与えておくと、ファイルの存在チェックをしてから文字列としてセットしてくれます。
ただし、click.File() とは違ってオープンはしてくれません。
code: bash
$ python click_argument_as_filepath.py /tmp/junk.txt
<class 'str'>
/tmp/junk.txt
$ rm /tmp/junk.txt
$ python click_argument_as_filepath.py /tmp/junk.txt
Usage: click_argument_as_filepath.py OPTIONS SRCFILE Try "click_argument_as_filepath.py --help" for help.
Error: Invalid value for "SRCFILE": Path "/tmp/junk.txt" does not exist.
コマンド引数を環境変数でも指定できるようにしたい
click.argument() に envvar=環境変数名を与えると、環境変数に設定されている文字列を引数としてセットします。
code: click_argument_envvar.py
import click
@click.command()
@click.argument('srcfile', envvar='SRCFILE', type=click.Path(exists=True))
def cmd(srcfile):
print(type(srcfile))
click.echo( click.format_filename(srcfile) )
if __name__ == '__main__':
cmd()
code: bash
$ echo 'Think Big.' > /tmp/junk.txt
$ python click_argument_envvar.py /tmp/junk.txt
<class 'str'>
/tmp/junk.txt
$ export SRCFILE=/tmp/junk.txt
$ python click_argument_envvar.py
<class 'str'>
/tmp/junk.txt
$ unset SRCFILE
$ python click_argument_envvar.py
Usage: click_argument_envvar.py OPTIONS SRCFILE Try "click_argument_envvar.py --help" for help.
Error: Missing argument "SRCFILE".
サブコマンド処理
@click.group() と@click.add_command() を使うと、git などのようなサブコマンドを持つアプリケーションを作ることができます。
code: click_subcommand.py
import click
@click.group()
def cli():
pass
@click.command()
def initdb():
click.echo('Initialized the database')
@click.command()
@click.option('--force', is_flag=True, default=True,
help='drop db anyway')
@click.argument('dbname')
def dropdb(force, dbname):
click.echo(force)
click.echo(f'Droped the database: {dbname}')
cli.add_command(initdb)
cli.add_command(dropdb)
if __name__ == "__main__":
cli()
code: bash
$ python click_subcommand_option.py
Usage: click_subcommand_option.py OPTIONS COMMAND ARGS... Options:
--help Show this message and exit.
Commands:
dropdb
initdb
$ python click_subcommand_option.py initdb
Initialized the database
$ python click_subcommand_option.py dropdb
Usage: click_subcommand_option.py dropdb OPTIONS NAME Try "click_subcommand_option.py dropdb --help" for help.
Error: Missing argument "NAME".
$ python click_subcommand_option.py dropdb --help
Usage: click_subcommand_option.py dropdb OPTIONS NAME Options:
--force drop db anyway
--help Show this message and exit.
$ python click_subcommand_option.py dropdb sample
True
Droped the database:sample
処理中にプログレスバーを表示させたい
少し時間がかかるような処理などで、click.progressbar() を使うとプログレスバーを表示してくれます。
code: click_progressbar.py
import click
import math
import time
import random
@click.command()
@click.option('--count', default=8000, type=click.IntRange(1, 100000),
help='The number of items to process.')
def cmd(count):
"""Demonstrates the progress bar."""
items = range(count)
def process_slowly(item):
time.sleep(0.002 * random.random())
def filter(items):
for item in items:
if random.random() > 0.3:
yield item
with click.progressbar(items, label='Processing accounts',
fill_char=click.style('#', fg='green')) as bar:
for item in bar:
process_slowly(item)
def show_item(item):
if item is not None:
with click.progressbar(filter(items), label='Committing transaction',
fill_char=click.style('#', fg='yellow'),
item_show_func=show_item) as bar:
for item in bar:
process_slowly(item)
with click.progressbar(length=count, label='Counting',
bar_template='%(label)s %(bar)s | %(info)s',
fill_char=click.style(u'█ ', fg='cyan'),
empty_char=' ') as bar:
for item in bar:
process_slowly(item)
with click.progressbar(length=count, width=0, show_percent=False,
show_eta=False,
fill_char=click.style('#', fg='magenta')) as bar:
for item in bar:
process_slowly(item)
# 'Non-linear progress bar'
count = int(sum(steps))
with click.progressbar(length=count, show_percent=False,
label='Slowing progress bar',
fill_char=click.style(u'█ ', fg='green')) as bar:
for item in steps:
time.sleep(item)
bar.update(item)
if __name__ == '__main__':
cmd()
オプション解析処理の情報を関数に渡したい
通常は、click がオプションや引数を解析をした結果を変数にセットして関数に渡します。@click.pass_context を使うと、デコレートされた関数は、第1引数がコンテキストを受け取れるようになり、オプション解析で行われた情報へアクセスすることができるようになります。
code: click_passcontext.py
import click
@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
@cli.command()
@click.pass_context
def sync(ctx):
click.echo('Debug is %s' % (ctx.obj'DEBUG' and 'on' or 'off')) if __name__ == '__main__':
cli(obj={})
click アプリケーションのテスト
CliRunner を使うと、関数をコマンドラインスクリプトとして実行してくれます。
CliRunner.invoke()メソッドは、コマンドラインスクリプトを単独で実行し、出力をバイトデータとバイナリデータの両方として取り込みます。
返り値は、キャプチャされた出力データ、終了コード、およびオプションの例外が添付されたResultオブジェクトとなりあます。
前述した click_hello.py をもう一度みてみましょう。
code: click_hello.py
import click
@click.command()
@click.option('-C', '--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name', help='The person to greet.')
def hello(count, name):
"""COUNTで与えた回数だけHelloする"""
for x in range(count):
click.echo(f'Hello {name}')
if __name__ == '__main__':
hello()
これをテストするためのコードは次のようになります。
code: test_click_hello.py
from click.testing import CliRunner
from click_hello import hello
def test_hello():
runner = CliRunner()
result = runner.invoke(hello, '--name Peter')
assert result.exit_code == 0
assert result.output == 'Hello Peter!\n'
result = runner.invoke(hello, '--name Jack -C 2')
assert result.exit_code == 0
assert result.output == 'Hello Jack!\nHello Jack!\n'
if __name__ == '__main__':
test_hello()
invoke() メソッドの第1引数にテストしたい関数名、第2引数にコマンドラインオプションを与えます。テストする関数をコマンドスクリプトとして実行した結果は、Resultオブジェクトにセットされて戻されます。
exception: 例外が発生したときにセットされる例外情報
exit_code:終了コード
stdout: 標準出力をテキストとして取り込んだ文字列
stdout_bytes: 標準出力をバイナリとして取り込んだデータ
stderr: 標準エラー出力をテキストとして取り込んだ文字列
stderr_bytes:標準エラー出力をバイナリとして取り込んだデータ
output: 標準出力と同じ
参考: